#
# CS 143
#
# This file provides the NIC class definition.
#
# @author Henry Li
#

from SimPy.Simulation import *
import Queue

class NIC(Process):
    '''The NIC class represents a process which handles the logic in between 
    Link and Host/Router. It manages when packets are sent or received
    by using buffers.

    '''
    
    # Metrics metrics
    # What we log to, if it isn't None. Should NEVER BE NONE when running
    metrics = None
    
    # Constants used to determine if packet in queue is being received or sent
    __SEND_MARKER = 1
    __RECV_MARKER = 2
    
    
    def __init__(self, parent, link, ip, subnet, gateway,
                 max_buffer_send_size, max_buffer_recv_size,
                 processing_delay, max_avg_list_size=None):
        '''NIC class constructor.
        
        '''
        # Initialize parent
        Process.__init__(self)
    
        # Router/Host __parent
        # Link __link
        # Attach NIC to device and link
        self.__parent = parent
        self.__link = link
        
        # 4-tuple (int, int, int, int) __ip
        # 4-tuple (int, int, int, int) __subnet
        # 4-tuple (int, int, int, int) __gateway
        # NIC identification
        self.__ip = ip
        self.__subnet = subnet
        self.__gateway = gateway
        
        # int __max_buffer_send_size
        # int __max_buffer_recv_size
        # Limit buffer size
        self.__max_buffer_send_size = max_buffer_send_size
        self.__max_buffer_recv_size = max_buffer_recv_size

        # float __processing_delay
        # Delay for receiving packets
        self.__processing_delay = float(processing_delay)
        
        # list __average_list
        # float __sum_processing_delay
        # int __num_data_points
        # int __max_avg_list_size
        # Data to calculate average processing delay
        self.__average_list = []
        self.__sum_processing_delay = 0.0
        self.__num_data_points = 0
        if max_avg_list_size:
            self.__max_avg_list_size = max_avg_list_size
        else:
            self.__max_avg_list_size = max_buffer_recv_size

        # PriorityQueue __buffer
        # int __buffer_send_size
        # int __buffer_recv_size
        # Priority queue for receiving and sending packets
        self.__buffer = Queue.PriorityQueue()
        self.__buffer_send_size = 0
        self.__buffer_recv_size = 0
        
        # int __last_sent_time
        # Time when last packet was sent
        self.__last_sent_time = 0
      
    def go(self):
        '''NIC class Process Execution Method.
        
        '''
        
        # Send/receive packets if any are available
        while True:
            while not self.__buffer.empty():
                (time, marker, data) = self.__buffer.get()

                # Packet is meant to be sent
                if marker == NIC.__SEND_MARKER:
                    
                    # Wait for transmission time
                    while time - now() > 0:
                        yield hold, self, time - now()
                    self.__buffer_send_size -= 1
                    (packet, gateway) = data
                    
                    # Log and send
                    self.metrics.log(self, now(), packet, "NICSendingIntoLink",
                                     self.__buffer_send_size)
                    self.__link.send(packet, gateway)
            
                # Packet is meant to be received
                elif marker == NIC.__RECV_MARKER:
                    
                    # Wait for processing delay
                    while time - now() > 0:
                        yield hold, self, time - now()
                    self.__buffer_recv_size -= 1
                    # data == packet 
                    self.__parent.receive(self, data)
                    
                    # Update moving average processing delay
                    delay = self.__processing_delay * self.__buffer_recv_size
                    self.__updateAvgProcessingDelay(delay)

            # No packets to send or receive right now
            yield passivate, self

    def send(self, packet):
        '''Enqueue packet into buffer with the time that it should be sent.
        
        Input:
            packet: Packet object
       
        '''
        # Check for buffer overflow
        if self.__buffer_send_size >= self.__max_buffer_send_size:
            print "Send buffer is full! We're dropping %d!" % packet.pid
            self.metrics.log(self, now(), packet, "Packet dropped")
        else:
            single_delay = float(packet.size) / self.__link.getLinkRate()
            
            # Time when packet should be transmitted
            if self.__buffer_send_size == 0:
                send_time = now() + single_delay
            else:
                send_time = self.__last_sent_time + single_delay

            # Keep track of when last packet is sent
            self.__last_sent_time = send_time
            self.__buffer_send_size += 1

            # Log and enqueue
            self.metrics.log(self, now(), packet, "NICSending",
                             self.__buffer_send_size)
            self.__buffer.put((send_time, NIC.__SEND_MARKER, 
                              (packet, self.__gateway)))
        
        # Reactivate in case we passivated
        if self.passive():
            reactivate(self)
        
    def receive(self, packet):
        '''Enqueue packet into buffer with the time it should be received.
        
        Input:
            packet: Packet object
       
        '''
        # Check for buffer overflow
        if self.__buffer_recv_size >= self.__max_buffer_recv_size:
            print "Receive buffer is full! We're dropping!"
            self.__metrics.log(self, now(), packet,
                               "Packet dropped when receiving")
        else:
            self.__buffer_recv_size += 1
            
            # Time when packet should be received
            delay = self.__processing_delay * self.__buffer_recv_size
            receive_time = now() + delay

            # Update moving average processing delay
            self.__updateAvgProcessingDelay(delay)

            # Log and enqueue
            self.metrics.log(self, now(), packet, "NICReceiving")
            self.__buffer.put((receive_time, NIC.__RECV_MARKER, packet))
        
        # Reactivate in case we passivated
        if self.passive():
            reactivate(self)
    
    def disconnect(self):
        '''Disconnects self and the parent device from the network.

        '''
        self.__link = None
        parent.disconnect(self)

    def getIP(self):
        '''Gets the ip address.
        
        Output:
            ip: 4 tuple representing an IP address
            
        '''
        return self.__ip

    def getSubnet(self):
        '''Gets the subnet mask.

        Output:
            subnet: 4 tuple representing a subnet mask
            
        '''
        return self.__subnet
    
    def getAvgProcessingDelay(self):
        '''Get the calculated average processing delay of receiving packets.
        
        Output:
            delay: float
        
        '''
        return self.__sum_processing_delay / self.__num_data_points \
               if self.__num_data_points else 0.0

    def getQueueingDelay(self):
        '''Get calculated queuing delay of sending packets.

        Output:
            delay: float.

        '''
        single_delay = float(1000) / self.__link.getLinkRate()
        if self.__buffer_send_size == 0:
            return single_delay
        return self.__last_sent_time - now() + single_delay

    def getPropDelay(self):
        '''Gets the propagation delay for the attached link.
        
        '''
        if self.__link:
            return self.__link.getPropDelay()
        return float('inf')
        
    def setGateway(self, gateway):
        '''Set the gateway ip address.
        
        Input:
            gateway: 4 tuple representing an IP address
        
        '''
        self.__gateway = gateway

    def __updateAvgProcessingDelay(self, delay):
        '''Add data point to the moving average processing delay.
        
        Input:
            delay: float
        
        '''
        # Shift window for moving average processing delay
        if self.__num_data_points >= self.__max_avg_list_size:
            self.__sum_processing_delay -= self.__average_list.pop(0)
            self.__sum_processing_delay += delay
            self.__average_list.append(delay)
        
        # Window not filled, so no need to shift
        else:
            self.__sum_processing_delay += delay
            self.__average_list.append(delay)
            self.__num_data_points += 1

